﻿﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;


using RecommenderSystem;
using RecommenderSystem.misc;
using RecommenderSystem.algorithms;

using RecommenderSystem.algorithms.modelBased;
using RecommenderSystem.algorithms.popularityBased;
//using log4net;
using RecommenderSystem.algorithms.memoryBased;
using RecommenderSystem.algorithms.itemBased;


namespace RecommenderSystem
{

    public class RecommenderSystem
    {
//        private static ILog _logger = log4net.LogManager.GetLogger(typeof(RecommenderSystem));

        private ICollection<IDataRecord> _dataset;
        private List<IDataRecord> _testDataset;
        private List<IDataRecord> _trainDataset;
        private Dictionary<string, Cache> cache;
        private Dictionary<string, User> _users;
        private Dictionary<string, Algoritm> _algs;


        public RecommenderSystem()
        {
            cache = new Dictionary<string, Cache>();
        }

        /**
         *  loads a dataset from a file
         */
        public void Load(string sFileName)
        {
            Load(sFileName, 0.9);

        }

        /**
         * loads a dataset from a file and divides the dataset to train and test 
         * according to dTrainSetSize parameter.
         * 
         */
        public void Load(string sFileName, double dTrainSetSize)
        {
            DateTime dtStart = DateTime.Now;
            Console.WriteLine("Starting Recommender System");
            this._dataset = Parser.getDataRecords(sFileName);
            _testDataset = new List<IDataRecord>();
            _trainDataset = new List<IDataRecord>();
            split(_dataset, _testDataset, _trainDataset, dTrainSetSize);
            this._users = generateUsers(this._trainDataset);
            this._algs = AlgorithmsFactory.getAlgorithms(this._users, this._trainDataset, this.cache);


            Console.WriteLine("Start time was " + Math.Round((DateTime.Now - dtStart).TotalSeconds, 0));
        }


        /**
         * 
         */
        public void TrainBaseModel(int cFeatures)
        {
            Console.WriteLine("Training Base Models");

            if (this._algs.ContainsKey("SVD"))
            {
                (this._algs["SVD"] as SVD).TrainBaseModel(cFeatures);
            }
            if (this._algs.ContainsKey("NNSVD"))
            {
                (this._algs["NNSVD"] as NNSVD).TrainBaseModel(cFeatures);
            }
           
            Console.WriteLine("Base Models Training Finished");
        }





        //return an existing rating 
        public double GetRating(string sUID, string sIID)
        {
            try
            {
                return this._users[sUID].getItemRate(sIID);
            }
            catch (Exception e)
            {
//                _logger.Error("Failed to get rating of user: " + sUID + " for item: " + sIID, e);
                return -1;
            }
        }

        //return an histogram of all ratings that the user has used
        public Dictionary<double, int> GetRatingsHistogram(string sUID)
        {
            User u = this._users[sUID];
            return u.GetRatingsHistogram();
        }

        //predict the rating that a user will give to an item using one of the methods "Pearson", "Cosine", "Random"
        public double PredictRating(string sMethod, string sUID, string sIID)
        {
            if (this._algs.Keys.Contains(sMethod))
            {
                return this._algs[sMethod].predictRating(sUID, sIID);
            }
            else
            {
                return -1;
            }
        }
        //return the predicted weights of all ratings that a user may give an item using one of the methods "Pearson", "Cosine", "Random"
        public Dictionary<double, double> PredictAllRatings(string sMethod, string sUID, string sIID)
        {
            Dictionary<double, double> allRatings = new Dictionary<double, double>();
            for (int i = 1; i <= 5; i++)
            {
                allRatings.Add(i, 0);
            }
            var recs = this._dataset.Where(rec => rec.getItemID().Equals(sIID)).ToList();
            foreach (IDataRecord record in recs)
            {
                allRatings[double.Parse(record.getItemRank())] += this._algs[sMethod].calculateSimilarity(this._users[sUID], this._users[record.getUserID()]);
            }

            double normalizeFactor = allRatings.Sum(a => a.Value);
            for (int i = 1; i <= 5; i++)
            {
                allRatings[i] = allRatings[i] / normalizeFactor;
            }

            return allRatings;
        }


        private double ComputeHitRatio(ICollection<IDataRecord> recs, Algoritm alg)
        {
            double sum = 0;
            foreach (IDataRecord rec in recs)
            {
                double rating = Math.Round(alg.predictRating(rec.getUserID(), rec.getItemID()), 0);
                if (rating == double.Parse(rec.getItemRank()))
                {
                    sum += 1;
                }
            }
            double ratio = sum / recs.Count();
            return ratio;
        }

        //Compute the hit ratio of all the methods in the list for a given train-test split (e.g. 0.95 train set size)
        public Dictionary<string, double> ComputeHitRatio(List<string> lMethods)
        {
            Dictionary<string, double> res = new Dictionary<string, double>();

            foreach (string method in lMethods)
            {
                double ratio = ComputeHitRatio(this._testDataset, this._algs[method]);
                res.Add(method, ratio);
            }

            return res;
        }
        public Dictionary<string, double> ComputeHitRatio(List<string> lMethods, out Dictionary<string, Dictionary<string, double>> dConfidence)
        {
            var result = new Dictionary<string, double>();

            var score = new Dictionary<string, Dictionary<string, double>>();
            dConfidence = new Dictionary<string, Dictionary<string, double>>();

            var userlist = this._testDataset.Select(rec => rec.getUserID()).Distinct().ToList();

            foreach (string method in lMethods)
            {
                result.Add(method, 0);
                score.Add(method, new Dictionary<string, double>());

                foreach (string othermeth in lMethods)
                {
                    if (!method.Equals(othermeth))
                    {
                        score[method].Add(othermeth, 0);
                    }
                }
            }

            // calculate hit ratio by each user
            foreach (string userID in userlist)
            {
                var recs = this._trainDataset.Where(rec => rec.getUserID().Equals(userID)).ToList();

                Dictionary<string, double> userres = new Dictionary<string, double>();

                foreach (string methodName in lMethods)
                {

                    double ratio = ComputeHitRatio(recs, this._algs[methodName]);

                    userres.Add(methodName, ratio);

                    result[methodName] += ratio * recs.Count(); // the sum of predicted ratings
                }

                // compare hit ratio of every 2 methods and update scores
                foreach (string method in lMethods)
                {
                    foreach (string othermeth in lMethods)
                    {
                        if (!method.Equals(othermeth))
                        {
                            if (userres[method] > userres[othermeth])
                                score[method][othermeth] += 1;
                            else if (userres[othermeth] > userres[method])
                                score[othermeth][method] += 1;
                            else
                            {
                                score[othermeth][method] += 0.5;
                                score[method][othermeth] += 0.5;
                            }
                        }
                    }
                }
            }


            // compute confidence & hit ratio from sums
            foreach (string method in lMethods)
            {
                result[method] = result[method] / this._testDataset.Count();
                foreach (string othermeth in lMethods)
                {
                    if (othermeth.Equals(method))
                        continue;
                    double k = score[method][othermeth];
                    double m = score[othermeth][method];
                    if (k >= m)
                    {
                        if (!dConfidence.Keys.Contains(method))
                        {
                            dConfidence.Add(method, new Dictionary<string, double>());
                        }
                        dConfidence[method].Add(othermeth, conf((int)k, (int)m));
                    }
                }

            }
            return result;
        }

        public List<string> Recommend(string sAlgorithm, string sUserId, int cRecommendations)
        {
            return this._algs[sAlgorithm].Recommend(sUserId, cRecommendations);
        }

        public Dictionary<int, Dictionary<string, Dictionary<string, double>>> ComputePrecisionRecall(List<string> lMethods, List<int> lLengths)
        {
            Dictionary<int, Dictionary<string, Dictionary<string, double>>> answer = new Dictionary<int, Dictionary<string, Dictionary<string, double>>>();
            /*Users in Test dataset*/
            Dictionary<string, User> usersInTestDB = generateUsers(_testDataset);

            foreach (int recommendationListLength in lLengths)
            {
                answer.Add(recommendationListLength, new Dictionary<string,Dictionary<string,double>>());
                foreach (string method in lMethods)
                {
                    if (Config.DEBUG_FLAG)
                    {
                        Console.WriteLine("Starting Algorithm: " + method + " recommendation length: " + recommendationListLength);   
                    }
                    DateTime dtStart = DateTime.Now;
                    answer[recommendationListLength].Add(method, new Dictionary<string,double>());
                    answer[recommendationListLength][method].Add(Config.PRECISION, 0);
                    answer[recommendationListLength][method].Add(Config.RECALL, 0);
                    double tp, fp, fn;
                    List<double> precisionResults = new List<double>();
                    List<double> recallResults = new List<double>();
                    foreach (User user in usersInTestDB.Values)
                    {
                        var consealedItems = user.getRatedItemsIds();
                        var revealedItems = _users[user.getUserId()].getRatedItemsIds();
                        double totalRatedItems = revealedItems.Count() + consealedItems.Count();
                        double itemsInTest = consealedItems.Count();
                        
                        if (Config.DEBUG_FLAG & itemsInTest / totalRatedItems > 0.5)
                        {
                            continue;
                        }
 
                       if (!_algs.ContainsKey(method))
                        {
                            break;
                        }
                        var recommendedItems = _algs[method].Recommend(user.getUserId(), recommendationListLength);                        
                        tp = (recommendedItems.Intersect(consealedItems)).Count();



                        if (Config.DEBUG_FLAG )
                        {
                            foreach (string itemId in recommendedItems)
                            {
                                Console.WriteLine("Item: " + itemId + " prediction is: " + PredictRating(method, user.getUserId(), itemId));
                            }
                        }

                        fp = recommendationListLength - tp;
                        fn = (int)itemsInTest - tp;
//                        if (_logger.IsDebugEnabled && Config.DEBUG_FLAG)
//                        {
//                            _logger.Debug("User " + user.getUserId() + " rated " + (revealedItems.Count() + consealedItems.Count()) + " items.");
//                           _logger.Debug("Train " + revealedItems.Count());
//                            _logger.Debug("Test " + consealedItems.Count());
//                            _logger.Debug("True Positive value is: " + tp);
//                            _logger.Debug("False Positive value is: " + fp);
//                            _logger.Debug("False Negative value is: " + fn);
//                        }
                        precisionResults.Add((tp / (tp + fp)));
                        recallResults.Add((tp / (tp + fn)));
                    }
                    //Count Average 
                    double recall, precision;
                    precision = precisionResults.Average();
                    recall = recallResults.Average();

                    answer[recommendationListLength][method][Config.PRECISION] = precision;
                    answer[recommendationListLength][method][Config.RECALL] = recall;
                    if (Config.DEBUG_FLAG)
                    {
                        Console.WriteLine("Execution time was " + Math.Round((DateTime.Now - dtStart).TotalSeconds, 0));
                    }
                  
                }
            }




            return answer;
        }

        public Dictionary<int, Dictionary<string, Dictionary<string, double>>> ComputePrecisionRecallAndSaveToExl(List<string> lMethods, List<int> lLengths, string fileName)
        {
            Dictionary<int, Dictionary<string, Dictionary<string, double>>> answer = ComputePrecisionRecall(lMethods, lLengths);

            StreamWriter allRes = new StreamWriter(fileName + ".csv");

            foreach (string method in lMethods)
            {
                allRes.WriteLine(method);
                allRes.WriteLine();
                allRes.WriteLine("N,Precision,Recall");
                foreach (int listLength in lLengths)
                {
                    allRes.WriteLine(listLength + "," + Math.Round(answer[listLength][method][Config.PRECISION],4) + "," + Math.Round(answer[listLength][method][Config.RECALL],4));

                }
                allRes.WriteLine();
            }
            allRes.Close();
            return answer;
        }


        private double computeRMSE(ICollection<IDataRecord> recs, Algoritm alg)
        {
            double predictedRating, realRating;
            double sum = 0;
            foreach (IDataRecord rec in recs)
            {
                predictedRating = alg.predictRating(rec.getUserID(), rec.getItemID());
                realRating = double.Parse(rec.getItemRank());
                sum += Math.Pow(predictedRating - realRating, 2);
            }
            double result = Math.Sqrt((sum / recs.Count()));
            return result;
        }

        public Dictionary<string, double> ComputeRMSE(List<string> lMethods)
        {

            Dictionary<string, double> result = new Dictionary<string, double>();
            foreach (string method in lMethods)
            {
                double rmse = computeRMSE(this._testDataset, this._algs[method]);
                result.Add(method, rmse);
            }

            return result;
        }

        public Dictionary<string, double> ComputeRMSEAndSaveToFile(List<string> lMethods, string fileName)
        {
            StreamWriter allRes = new StreamWriter(fileName+".csv");
            
            allRes.WriteLine("Method,RMSE");
            allRes.WriteLine();

            Dictionary<string, double> result = new Dictionary<string, double>();
            foreach (string method in lMethods)
            {

                double rmse = computeRMSE(this._testDataset, this._algs[method]);
                allRes.WriteLine(method+","+ Math.Round(rmse, 4));
                result.Add(method, rmse);
            }
            allRes.Close();
            return result;
        }




        public Dictionary<string, double> ComputeRMSE(List<string> lMethods, out Dictionary<string, Dictionary<string, double>> dConfidence)
        {

            var result = new Dictionary<string, double>();

            var score = new Dictionary<string, Dictionary<string, double>>();
            dConfidence = new Dictionary<string, Dictionary<string, double>>();

            var userlist = this._testDataset.Select(rec => rec.getUserID()).Distinct().ToList();

            // init dictionarys
            foreach (string method in lMethods)
            {
                result.Add(method, 0);
                score.Add(method, new Dictionary<string, double>());

                foreach (string othermeth in lMethods)
                {
                    if (!method.Equals(othermeth))
                    {
                        score[method].Add(othermeth, 0);
                    }
                }
            }

            // calculate RMSE by each user
            foreach (string userID in userlist)
            {
                var recs = this._trainDataset.Where(rec => rec.getUserID().Equals(userID)).ToList();

                Dictionary<string, double> userres = new Dictionary<string, double>();

                foreach (string methodName in lMethods)
                {

                    double rmse = computeRMSE(recs, this._algs[methodName]);

                    userres.Add(methodName, rmse);

                    result[methodName] += rmse * rmse * recs.Count(); // the error sum since rmse = sqrt(error sum/count)
                }

                // compare RMSE of every 2 methods and update scores
                foreach (string method in lMethods)
                {
                    foreach (string othermeth in lMethods)
                    {
                        if (!method.Equals(othermeth))
                        {
                            if (userres[method] < userres[othermeth])
                                score[method][othermeth] += 1;
                            else if (userres[othermeth] < userres[method])
                                score[othermeth][method] += 1;
                            else
                            {
                                score[othermeth][method] += 0.5;
                                score[method][othermeth] += 0.5;
                            }
                        }
                    }
                }
            }


            // compute confidence and RMSE from sums
            foreach (string method in lMethods)
            {
                result[method] = Math.Sqrt((result[method] / this._testDataset.Count()));
                foreach (string othermeth in lMethods)
                {
                    if (othermeth.Equals(method))
                        continue;
                    double k = score[method][othermeth];
                    double m = score[othermeth][method];
                    if (k >= m)
                    {
                        if (!dConfidence.Keys.Contains(method))
                        {
                            dConfidence.Add(method, new Dictionary<string, double>());
                        }
                        dConfidence[method].Add(othermeth, conf((int)k, (int)m));
                    }
                }

            }
            return result;
        }

        private Dictionary<string, User> generateUsers(ICollection<IDataRecord> dataset)
        {
            Dictionary<string, User> users = new Dictionary<string, User>();
            foreach (IDataRecord rec in dataset)
            {
                User u = null;
                if (!users.Keys.Contains(rec.getUserID()))
                {
                    u = new User(rec.getUserID());
                    users.Add(rec.getUserID(), u);
                }
                else
                {
                    u = users[rec.getUserID()];
                }
                u.addItem(rec.getItemID(), rec.getItemRank());
            }
            return users;

        }


        /**
         * Splits the given dataset to train & test datasets.
         * p * 100 records goes to the test set and the rest goes to the train set.
         * Pre condition: parameter p is between 0 to 1. 
         * 
         */
        public static void split(ICollection<IDataRecord> dataset, List<IDataRecord> test, List<IDataRecord> train, double p)
        {
            //pre condition
            if (p < 0 | p > 1) { throw new ArgumentOutOfRangeException("p"); }

            Random r = new Random();

            var users = dataset.Select(rec => rec.getUserID()).Distinct().ToList();

            while (test.Count() < (1 - p) * dataset.Count())
            {
                // chose random user
                var user = users.ElementAt(r.Next(users.Count()));
                users.Remove(user);

                // choose users records
                var recs = dataset.Where(rec => rec.getUserID().Equals(user));

                // choose random k records
                int k = r.Next(recs.Count());
                var selected = sample(recs.ToList(), k);

                test.AddRange(selected);
                train.AddRange(recs.Except(selected));
            }

            var rest = dataset.Where(rec => users.Contains(rec.getUserID()));
            train.AddRange(rest);
        }

        private List<double> userRatings(string userID)
        {
            return this._dataset.Where((rec) => rec.getData(Config.USER_ID).Equals(userID)).OrderBy((rec) => rec.getData(Config.ITEM_ID)).Select((rec) => double.Parse(rec.getData(Config.ITEM_RANK))).ToList<double>();
        }

        private IEnumerable<string> userItems(string userID)
        {
            return this._dataset.Where((rec) => rec.getData(Config.USER_ID).Equals(userID)).Select((rec) => rec.getData(Config.ITEM_ID));
        }



        private IEnumerable<double> getRatings(string userID, IEnumerable<string> items)
        {
            return items.Select((item) =>
            {
                if (this._dataset.Any((rec) => rec.getData(Config.USER_ID).Equals(userID) && rec.getData(Config.ITEM_ID).Equals(item)))
                {
                    return double.Parse(this._dataset.Where((rec) => rec.getData(Config.USER_ID).Equals(userID) && rec.getData(Config.ITEM_ID).Equals(item)).First().getData(Config.ITEM_RANK));
                }
                else
                {
                    return 0;
                }
            }
            );
        }

        private static List<T> sample<T>(List<T> set, int size)
        {
            List<T> res = new List<T>();
            Random r = new Random();
            for (int i = 0; i < set.Count(); i++)
            {
                if (size == 0)
                {
                    break;
                }
                if (r.NextDouble() < (size / (double)(set.Count() - i)))
                {
                    res.Add(set.ElementAt(i));
                    size--;
                }
            }

            return res;
        }

        // confidence formula
        public double conf(int k, int m)
        {
            double a, b, c, result = 0d;
            a = sumLogsInRange(k + m);
            for (int i = k; i <= k + m; i++)
            {
                b = sumLogsInRange(i);
                c = sumLogsInRange(k + m - i);

                result += Math.Pow(10, a - b - c);
            }

            return result * Math.Pow(0.5, k + m);
        }

        private double sumLogsInRange(int range)
        {
            double result = 0d;
            for (int i = 1; i <= range; i++)
            {
                result += Math.Log10(i);
            }
            return result;
        }
    }

}